【SwiftUI】UIImagePickerControllerとPHPickerViewControllerを使ってカメラとフォトライブラリから動画を取得する
SwiftUIで端末のカメラで撮影した動画、またはフォトライブラリにある動画を取得したかった為、調べました。
以前からUIImagePickerController
にはお世話になっていましたが、UIImagePickerController.SourceType.photoLibrary
はiOS 14より新しいOSバージョンでは非推奨になっている為、フォトライブラリからソースを取り出す場合はPHPickerViewController
の使用が推奨されております。
なので今回はカメラから動画を取得する場合は、UIImagePickerController
を使用して、フォトアルバムから動画を取得する場合は、PHPickerViewController
を使用する方法を調べてみました。
環境
- Xcode 13.3
- iOS 15.4.1
作ったもの
カメラで撮影した動画、またはフォトライブラリに保存してある動画を取得してVideoPlayer
で再生するアプリです。
カメラ撮影した動画を取得する
カメラで撮影した動画を取得する為に、UIImagePickerController
を使用します。SwiftUIで使用する為にUIViewControllerRepresentable
に準拠させたstruct
を作成します。
import UniformTypeIdentifiers import SwiftUI struct CameraMoviePickerView: UIViewControllerRepresentable { @Environment(\.dismiss) private var dismiss @Binding var movieUrl: URL? func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera picker.delegate = context.coordinator picker.mediaTypes = [UTType.movie.identifier] picker.videoQuality = .typeHigh return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> Coordinator { return Coordinator(self) } class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let parent: CameraMoviePickerView init(_ parent: CameraMoviePickerView) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else { return } parent.movieUrl = movieURL parent.dismiss() } func imagePickerControllerDidCancel(_: UIImagePickerController) { parent.dismiss() } } }
内容については説明していきます。
プロパティ
ImagePickerで動画を取得、または取得をキャンセルした場合にViewを破棄する為に環境変数dismiss
を用意します。
@Environment(\.dismiss) private var dismiss
動画のURL
を取得した時にバインディングしたいのでバインディング変数を定義します。
@Binding var movieUrl: URL?
makeUIViewController
makeUIViewController
で今回生成するUIImagePickerController
の設定を行い、返り値として渡します。
func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera picker.mediaTypes = [UTType.movie.identifier] picker.videoQuality = .typeHigh picker.delegate = context.coordinator return picker }
- sourceType
- 今回はカメラからの動画を取得するので
.camera
を指定しています。
- 今回はカメラからの動画を取得するので
- mediaTypes
- 今回はメディアは動画を取得したいので、
[UTType.movie.identifier]
を指定します。
- 今回はメディアは動画を取得したいので、
- videoQuality
- 特に指定はないですが、今回は高品質動画
.typeHigh
にしています。
- 特に指定はないですが、今回は高品質動画
- delegate
UIViewControllerRepresentable
でdelegateメソッドを呼ぶ為に、context.coordinator
を代入しています。
makeCoordinator
ViewControllerのイベントをSwiftUIに伝達する役割を果たすCoordinator
を生成します。
func makeCoordinator() -> Coordinator { return Coordinator(self) }
Coordinator
UIImagePickerControllerDelegate
を使用する為にUINavigationControllerDelegate
とUIImagePickerControllerDelegate
に準拠しています。
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let parent: CameraMoviePickerView init(_ parent: CameraMoviePickerView) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else { return } parent.movieUrl = movieURL parent.dismiss() } func imagePickerControllerDidCancel(_: UIImagePickerController) { parent.dismiss() } }
parent
UIImagePickerController
のイベント伝達を受け取る変数になります。
let parent: CameraMoviePickerView
imagePickerController(_:, didFinishPickingMediaWithInfo:)
UIImagePickerController
でメディア(今回は動画)が選択された時に呼ばれるメソッドです。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else { return } parent.movieUrl = movieURL parent.dismiss() }
選択された動画のURL
を取得して、CameraMoviePicker
のmovieUrl
に代入しています。その後、dismiss
を行い、画面を閉じるようにしています。
imagePickerControllerDidCancel(_:)
UIImagePickerController
のキャンセルボタンが押された時に呼ばれるメソッドなので、dismiss
を行い、画面を閉じています。
func imagePickerControllerDidCancel(_: UIImagePickerController) { parent.dismiss() }
これでUIImagePickerController
を使用してカメラで撮影した動画を取得出来ました。
フォトライブラリから動画を取得する
冒頭で説明した通り、iOS 14.0より新しいバージョンでは、UIImagePickerController.SourceType.photoLibrary
は非推奨になっている為、フォトアルバムの動画を取得するのにPHPickerViewController
を使用します。
PHPickerViewController
はUIImagePickerController
の代替えクラスで、安定性と信頼性が向上しています。開発者とユーザーは複数の恩恵を受けることが出来るそうです。詳細はPHPickerViewController
をご覧下さい。
import SwiftUI import PhotosUI struct PhotoLibraryMoviePickerView: UIViewControllerRepresentable { @Environment(\.dismiss) private var dismiss @Binding var movieUrl: URL? func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration() configuration.filter = .videos configuration.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: PhotoLibraryMoviePickerView init(_ parent: PhotoLibraryMoviePickerView) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.dismiss() guard let provider = results.first?.itemProvider else { return } let typeIdentifier = UTType.movie.identifier if provider.hasItemConformingToTypeIdentifier(typeIdentifier) { provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in if let error = error { print("error: \(error)") return } if let url = url { let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)" let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) try? FileManager.default.copyItem(at: url, to: newUrl) self.parent.movieUrl = newUrl } } } } } }
内容について説明していきます。
プロパティ
PHPickerViewController
で動画を取得、または取得をキャンセルした場合にViewを破棄する為に環境変数dismiss
を用意します。
@Environment(\.dismiss) private var dismiss
動画のURL
を取得した時にバインディングしたいのでバインディング変数を定義します。
@Binding var movieUrl: URL?
makeUIViewController
makeUIViewController
で今回生成するPHPickerViewController
の設定を行い、返り値として渡します。
PHPickerViewController
のインスタンス生成にはPHPickerConfiguration
が必要なので、まずはPHPickerConfiguration
の設定を行い、その構成と共にPHPickerViewController
を生成します。
func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration() configuration.filter = .videos configuration.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator return picker }
- configuration.filter
- ピッカーがどのアセットタイプを表示させるかのフィルタリングです。今回は動画なので
.video
を指定しています。
- ピッカーがどのアセットタイプを表示させるかのフィルタリングです。今回は動画なので
- configuration.preferredAssetRepresentationMode
- アセットに複数の表現が含まれている場合に使用する表現を決定するモードです。preferredAssetRepresentationModeのドキュメントに記載があるのですが、システムが追加のトランスコーディングを実行して、要求したアセットを互換性のある表現に変換する場合がある為、可能であれば、トランスコーディングを回避するために
.current
を使用します。
- アセットに複数の表現が含まれている場合に使用する表現を決定するモードです。preferredAssetRepresentationModeのドキュメントに記載があるのですが、システムが追加のトランスコーディングを実行して、要求したアセットを互換性のある表現に変換する場合がある為、可能であれば、トランスコーディングを回避するために
- configuration.selectionLimit
- 何個選択することが出来るかを指定することが出来ます。デフォルト値は
1
で、今回は1つしか取得しない為、デフォルトを使用するので特に指定していません。
- 何個選択することが出来るかを指定することが出来ます。デフォルト値は
Coordinator
makeCoordinator
の箇所はUIImagePickerController
の時と変わりがない為、割愛します。
Coordinator
クラスについて説明していきます。
class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: PhotoLibraryMoviePickerView init(_ parent: PhotoLibraryMoviePickerView) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.dismiss() guard let provider = results.first?.itemProvider else { return } let typeIdentifier = UTType.movie.identifier if provider.hasItemConformingToTypeIdentifier(typeIdentifier) { provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in if let error = error { print("error: \(error)") return } if let url = url { let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)" let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) try? FileManager.default.copyItem(at: url, to: newUrl) self.parent.movieUrl = newUrl } } } } }
parent
PHPickerViewController
のイベント伝達を受ける変数になります。
let parent: PhotoLibraryMoviePickerView
picker(_:, didFinishPicking:)
PHPickerViewController
でメディアが選択された時に呼ばれるメソッドです。PHPickerViewController
では、didCancel
のようなメソッドはなく、キャンセルが押された際もこのメソッドが呼ばれます。
なので、メソッドが呼ばれた場合にまずdismiss
を行い、メディアのURL
が受け取れた場合は、値をバインディングするようにしています。
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.dismiss() guard let provider = results.first?.itemProvider else { return } let typeIdentifier = UTType.movie.identifier if provider.hasItemConformingToTypeIdentifier(typeIdentifier) { provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in if let error = error { print("error: \(error)") return } if let url = url { let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)" let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) try? FileManager.default.copyItem(at: url, to: newUrl) self.parent.movieUrl = newUrl } } } }
今回は動画を取得したいのでtypeIdentifier
には、オーディオとビデオを含むメディアを表すUTType.movie.identifier
を代入しています。
provider.hasItemConformingToTypeIdentifier(typeIdentifier)
でオーディオとビデオを含むメディアであるかを判定して、該当メディアである場合はURL
取得処理を進めていきます。
provider.loadFileRepresentation(forTypeIdentifier:)
で取得したファイルのデータのコピーを一時ファイルに書き込みます。一時ファイルは、完了ハンドラーが戻ったときにシステムが削除する為、ファイルのデータをTemporaryDirectory
にコピーしています。
let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)" let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) try? FileManager.default.copyItem(at: url, to: newUrl) self.parent.movieUrl = newUrl
これでPHPickerViewController
を使用してフォトライブラリから動画を取得出来ました。
MoviePlayerView
動画の再生には、AVKit
のVideoPlayer
を使用します。引数にAVPlayer
を渡すことでコンテンツの再生を制御出来ます。またVideoPlayer
には閉じるボタンが無いため、Viewの上部に閉じるボタンを追加しました。
import SwiftUI import AVKit struct MoviePlayerView: View { @Environment(\.dismiss) private var dismiss private let movieUrl: URL? init(with movieUrl: URL?) { self.movieUrl = movieUrl } var body: some View { VStack { HStack { Spacer() Button { dismiss() } label: { Text("Close") } Spacer() .frame(width: 16) } VideoPlayer(player: AVPlayer(url: movieUrl!)) } } }
ContentView
各Pickerからの動画URLを保持するState変数movieUrl
と、各PickerとMoviePlayerView
を表示するかどうかのフラグのBool値を用意しています。
各Picker用のButtonを押すと、それに紐づくPickerが表示されます。movieUrl
がnil
でない場合は、再生ボタンが活性化し、押すと動画が再生されます。
import SwiftUI struct ContentView: View { @State private var movieUrl: URL? @State private var showCameraMoviePickerView = false @State private var showPhotoLibraryMoviePickerView = false @State private var showMoviePlayerView = false private var canPlayVideo: Bool { movieUrl != nil } var body: some View { VStack(spacing: 32) { Spacer() Button { showCameraMoviePickerView = true } label: { Text("Camera Movie Picker") } Button { showPhotoLibraryMoviePickerView = true } label: { Text("Photo Library Movie Picker") } Button { showMoviePlayerView = true guard let url = movieUrl else { return } print(url) } label: { Image(systemName: "play") .resizable() .frame(width: 50, height:50) .foregroundColor(canPlayVideo ? .accentColor : .gray) } .disabled(!canPlayVideo) Spacer() } .fullScreenCover(isPresented: $showCameraMoviePickerView) { CameraMoviePickerView(movieUrl: $movieUrl) } .fullScreenCover(isPresented: $showPhotoLibraryMoviePickerView) { PhotoLibraryMoviePickerView(movieUrl: $movieUrl) } .fullScreenCover(isPresented: $showMoviePlayerView) { MoviePlayerView(with: movieUrl) } } }
これで完成です!
おわりに
これまではUIImagePickerController
を使っていましたが、これからは積極的にPHPickerViewController
を使っていきたいですね!
参考
- UIImagePickerController.SourceType.photoLibrary
- UIImagePickerController
- PHPickerViewController
- preferredAssetRepresentationMode
- PHPickerViewControllerを使った動画の読み込み方法
- SwiftUI - How to load a video from Photos
- [SwiftUI][iOS]UIKitの使い方(2)〜Coordinator(コーディネータ)編〜
- movie
- loadFileRepresentation(forTypeIdentifier:completionHandler:)